// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Macios.Generator.Attributes;
using Microsoft.Macios.Generator.DataModel;
using Microsoft.Macios.Generator.Formatters;
using static Microsoft.Macios.Generator.Emitters.BindingSyntaxFactory;

namespace Microsoft.Macios.Generator.IO;

static class TabbedStringBuilderExtensions {
	/// <summary>
	/// Append the generated code attribute to the current string builder. Added for convenience.
	/// </summary>
	/// <param name="self">A tabbed string builder.</param>
	/// <param name="optimizable">If the binding is Optimizable or not.</param>
	/// <returns>The current builder.</returns>
	public static TabbedWriter<StringWriter> AppendGeneratedCodeAttribute (this TabbedWriter<StringWriter> self,
		bool optimizable = true)
	{
		if (optimizable) {
			const string attr = "[BindingImpl (BindingImplOptions.GeneratedCode | BindingImplOptions.Optimizable)]";
			self.WriteLine (attr);
		} else {
			const string attr = "[BindingImpl (BindingImplOptions.GeneratedCode)]";
			self.WriteLine (attr);
		}

		return self;
	}

	/// <summary>
	/// Append the notification advice attribute to the current string builder. Added for convenience.
	/// </summary>
	/// <param name="self">A tabbed string builder.</param>
	/// <param name="className">The class that contains the notification.</param>
	/// <param name="notification">The name of the notification.</param>
	/// <returns></returns>
	public static TabbedWriter<StringWriter> AppendNotificationAdvice (this TabbedWriter<StringWriter> self,
		in string className, in string notification)
	{
		string attr =
			$"[Advice (\"Use '{className}.Notifications.{notification}' helper method instead.\")]";
		self.WriteLine (attr);
		return self;
	}

	/// <summary>
	/// Append a EditorBrowsable attribute. Added for convenience.
	/// </summary>
	/// <param name="self">A tabbed string builder.</param>
	/// <returns>The current builder.</returns>
	public static TabbedWriter<StringWriter> AppendEditorBrowsableAttribute (this TabbedWriter<StringWriter> self,
		EditorBrowsableState state)
	{
		string attr = $"[EditorBrowsable (EditorBrowsableState.{state})]";
		self.WriteLine (attr);
		return self;
	}

	/// <summary>
	/// Append the designated initializer attribute to the current string builder. Added for convenience. 
	/// </summary>
	/// <param name="self">A tabbed string builder.</param>
	/// <returns>The current builder.</returns>
	public static TabbedWriter<StringWriter> AppendDesignatedInitializer (this TabbedWriter<StringWriter> self)
	{
		const string attr = "[DesignatedInitializer]";
		self.WriteLine (attr);
		return self;
	}

	/// <summary>
	/// Writes the autogenerated header and other pragmas.
	/// </summary>
	/// <param name="self">A tabbed string builder.</param>
	/// <returns>The current builder.</returns>
	public static TabbedWriter<StringWriter> WriteHeader (this TabbedWriter<StringWriter> self)
	{
		// let people know this is generated code
		self.WriteLine ("// <auto-generated />");

		// enable nullable!
		self.WriteLine ();
		self.WriteLine ("#nullable enable");
		self.WriteLine ();
		return self;
	}

	/// <summary>
	/// Appends a `[return: DelegateProxy]` attribute to the current writer.
	/// This attribute is used for properties that return a delegate, and it points to the static bridge class
	/// generated for the specified delegate type.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="typeInfo">The <see cref="TypeInfo"/> of the delegate.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendDelegateProxyReturn (this TabbedWriter<StringWriter> self,
		in TypeInfo typeInfo)
	{
		var staticBridge =
			Nomenclator.GetTrampolineClassName (typeInfo, Nomenclator.TrampolineClassType.StaticBridgeClass);
		self.WriteLine ($"[return: DelegateProxy (typeof ({Trampolines}.{staticBridge}))]");
		return self;
	}

	/// <summary>
	/// Appends a `[param: BlockProxy]` attribute to the current writer.
	/// This attribute is used for parameters that are delegates (blocks), and it points to the native invocation class
	/// generated for the specified delegate type.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="typeInfo">The <see cref="TypeInfo"/> of the delegate.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendDelegateParameter (this TabbedWriter<StringWriter> self,
		in TypeInfo typeInfo)
	{
		var nativeInvoker =
			Nomenclator.GetTrampolineClassName (typeInfo, Nomenclator.TrampolineClassType.NativeInvocationClass);
		self.WriteLine ($"[param: BlockProxy (typeof ({Trampolines}.{nativeInvoker}))]");
		return self;
	}

	/// <summary>
	/// Appends a `[Preserve]` attribute to the current writer.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="conditional">A value indicating whether the preservation is conditional.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendPreserveAttribute (this TabbedWriter<StringWriter> self,
		bool conditional = true)
	{
		self.WriteLine ($"[Preserve (Conditional = {conditional.ToString ().ToLower ()})]");
		return self;
	}

	/// <summary>
	/// Appends a `[DynamicDependency]` attribute to the current writer.
	/// This attribute is used to indicate that a member has a dynamic dependency on another member.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="member">The member that is dynamically depended upon.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendDynamicDependencyAttribute (this TabbedWriter<StringWriter> self,
		string member)
	{

		self.WriteLine ($"[DynamicDependency (\"{member}\")]");
		return self;
	}

	/// <summary>
	/// Appends a `[DynamicDependency]` attribute to the current writer.
	/// This attribute is used to indicate that a member has a dynamic dependency on another member.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="method">The method that is dynamically depended upon.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendDynamicDependencyAttribute (this TabbedWriter<StringWriter> self,
		in Method method)
	{
		var sb = new StringBuilder ();
		sb.Append (method.Name);
		sb.Append ('(');
		sb.AppendJoin (',', method.Parameters.Select (p => p.Type.GetIdentifierSyntax (useGlobalNamespace: false)));
		sb.Append (')');
		self.WriteLine ($"[DynamicDependency (\"{sb.ToString ()}\")]");
		return self;
	}

	/// <summary>
	/// Appends a `[ProtocolMember]` attribute to the current writer.
	/// This attribute contains metadata about a protocol member (method or property).
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="protocolMemberData">The protocol member metadata to emit.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendProtocolMemberData (this TabbedWriter<StringWriter> self,
		in ProtocolMemberData protocolMemberData)
	{
		// shared properties 
		var sb = new StringBuilder ("[ProtocolMember (");
		sb.Append ($"IsRequired = {protocolMemberData.IsRequired.ToString ().ToLower ()}, ");
		sb.Append ($"IsProperty = {protocolMemberData.IsProperty.ToString ().ToLower ()}, ");
		sb.Append ($"IsStatic = {protocolMemberData.IsStatic.ToString ().ToLower ()}, ");
		sb.Append ($"Name = \"{protocolMemberData.Name}\", ");
		sb.Append ($"Selector = \"{protocolMemberData.Selector}\", ");
		if (protocolMemberData.IsProperty) {
			sb.Append ($"PropertyType = typeof ({protocolMemberData.PropertyType!.Value.GetIdentifierSyntax ()}), ");
			sb.Append ($"GetterSelector = {Quoted (protocolMemberData.GetterSelector)}, ");
			sb.Append ($"SetterSelector = {Quoted (protocolMemberData.SetterSelector)}, ");
			sb.Append ($"ArgumentSemantic = ArgumentSemantic.{protocolMemberData.ArgumentSemantic}");
		} else {
			sb.Append ($"ReturnType = typeof ({protocolMemberData.ReturnType!.Value.GetIdentifierSyntax ()}), ");
			// build the parameter types string array
			var paramTypes = string.Join (", ",
				protocolMemberData.ParameterType.Select (x => $"typeof ({x.GetIdentifierSyntax ()})"));
			sb.Append ($"ParameterType = new Type [] {{ {paramTypes} }}, ");

			// build the parameters ref array
			var parametersRef = string.Join (", ",
				protocolMemberData.ParameterByRef.Select (x => x ? "true" : "false"));
			sb.Append ($"ParameterByRef = new bool [] {{ {parametersRef} }}");
		}

		// the following are optional since we might not have a proxy for a callback but are used in both cases (methods and properties)
		if (protocolMemberData.ReturnTypeDelegateProxy is not null) {
			sb.Append (
				$", ReturnTypeDelegateProxy = typeof ({protocolMemberData.ReturnTypeDelegateProxy.Value.GetIdentifierSyntax ()})");
		}

		if (protocolMemberData.ParameterBlockProxy.Length > 0) {
			// build the parameter block proxy string array
			var blockProxies = string.Join (", ",
				protocolMemberData.ParameterBlockProxy.Select (x =>
					x is null ? "null" : $"typeof ({x.Value.GetIdentifierSyntax ()})"));
			sb.Append ($", ParameterBlockProxy = new Type? [] {{ {blockProxies} }}");
		}

		sb.Append (")]");
		self.WriteLine (sb.ToString ());
		return self;

		string Quoted (string? str)
		{
			return str is null ? "null" : $"\"{str}\"";
		}
	}

	/// <summary>
	/// Appends a `[Protocol]` attribute to the current writer.
	/// This attribute is used to mark an interface as representing an Objective-C protocol.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="name">The name of the Objective-C protocol.</param>
	/// <param name="wrapperName">The name of the wrapper type for the protocol.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendProtocolAttribute (this TabbedWriter<StringWriter> self,
		string name, string wrapperName)
	{
		self.WriteLine ($"[Protocol (Name = \"{name}\", WrapperType = typeof ({wrapperName}))]");
		return self;
	}

	/// <summary>
	/// Appends a `[Foundation.RequiredMember]` attribute to the current writer.
	/// This attribute is used to mark a member as required.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendRequiredMemberAttribute (this TabbedWriter<StringWriter> self)
	{
		self.WriteLine ($"[{RequiredMember}]");
		return self;
	}

	/// <summary>
	/// Appends an `[Export<Method>]` attribute to the current writer.
	/// This is a simplified version that only includes the selector, as required by the registrar.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="exportData">The export data for the method, from which the selector is extracted.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendExportAttribute (this TabbedWriter<StringWriter> self,
		ExportData<ObjCBindings.Method> exportData)
	{
		// the resitrar does not care about other flags but the selector, so we can just use the selector
		self.WriteLine ($"[Export<Method> (\"{exportData.Selector}\")]");
		return self;
	}

	/// <summary>
	/// Appends an `[Export<Property>]` attribute to the current writer.
	/// This is a simplified version that only includes the selector, as required by the registrar.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="exportData">The export data for the property, from which the selector is extracted.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendExportAttribute (this TabbedWriter<StringWriter> self,
		ExportData<ObjCBindings.Property> exportData)
	{
		// the registrar does not care about other flags but the selector, so we can just use the selector
		self.WriteLine ($"[Export<Property> (\"{exportData.Selector}\")]");
		return self;
	}

	/// <summary>
	/// Appends an `[Export]` attribute to the current writer for bgen compatibility.
	/// This is a simplified version that only includes the selector.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="exportData">The export data for the method, from which the selector is extracted.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendBgenExportAttribute (this TabbedWriter<StringWriter> self,
		ExportData<ObjCBindings.Method> exportData)
	{
		// append the old export attribute for bgen compatibility so that we do not need to update the registrar right
		// away
		self.WriteLine ($"[Export (\"{exportData.Selector}\")]");
		return self;
	}

	/// <summary>
	/// Appends an `[Export]` attribute to the current writer for bgen compatibility.
	/// This is a simplified version that only includes the selector.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="selector">The selector for the export attribute.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendBgenExportAttribute (this TabbedWriter<StringWriter> self,
		string? selector)
	{
		if (string.IsNullOrEmpty (selector))
			return self;

		// append the old export attribute for bgen compatibility so that we do not need to update the registrar right
		// away
		self.WriteLine ($"[Export (\"{selector}\")]");
		return self;
	}

	/// <summary>
	/// Appends an `[Export]` attribute to the current writer for bgen compatibility.
	/// This is a simplified version that only includes the selector.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="exportData">The export data for the property, from which the selector is extracted.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendBgenExportAttribute (this TabbedWriter<StringWriter> self,
		ExportData<ObjCBindings.Property> exportData)
	{
		// append the old export attribute for bgen compatibility so that we do not need to update the registrar right
		// away
		self.WriteLine ($"[Export (\"{exportData.Selector}\")]");
		return self;
	}

	/// <summary>
	/// Appends a `[Register]` attribute to the current writer.
	/// This attribute is used to register a type with the Objective-C runtime.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <param name="registrationName">The name to register the type with.</param>
	/// <param name="isWrapper">A value indicating whether the type is a wrapper around a native type.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendRegisterAttribute (this TabbedWriter<StringWriter> self,
		string registrationName, bool isWrapper)
	{
		self.WriteLine ($"[Register (\"{registrationName}\", {isWrapper.ToString ().ToLower ()})]");
		return self;
	}

	/// <summary>
	/// Appends a `[Register]` attribute to the current writer.
	/// This attribute is used to register a type with the Objective-C runtime using its C# name.
	/// </summary>
	/// <param name="self">A tabbed string writer.</param>
	/// <returns>The current writer.</returns>
	public static TabbedWriter<StringWriter> AppendRegisterAttribute (this TabbedWriter<StringWriter> self)
	{
		self.WriteLine ("[Register]");
		return self;
	}
}
